/**
 * 
 */
package engine;

import gui.GameMenuBar;
import gui.PitchGUI;
import gui.PitchGUI.ButtonCardState;
import gui.customControl.ActionDialog;
import gui.customControl.BlockActionDialog;
import gui.customControl.LogWindow;
import gui.customControl.PushActionDialog;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.JOptionPane;

import ai.AIEngine;

import common.Dice;

import dao.DataLoader;
import data.Data.ActionType;
import data.Data.BlockActionType;
import data.Data.Direction;
import data.Data.PlayerState;
import data.Data.Race;
import data.Data.Skill;
import data.Data.TeamID;
import data.Data.Zones;
import data.Player;
import data.Position;
import data.Team;
import data.selection.BlockSelectionObject;
import data.selection.MoveSelectionObject;
import data.selection.SelectionObject;
import data.selection.ThrowSelectionObject;
import exception.BadPositionException;
import exception.IllegalPlacementException;
import exception.TeamException;

/**
 * Singleton. The core of the game. It controls the gameplay, keeps track of the
 * teams, rules, score, etc.
 * 
 * @author bogdan
 */
public class GameEngine implements Serializable, IGameEngine {

	private static final long serialVersionUID = 601995837186082962L;

	private static final String GUI_CAPTION = "Blood Bowl";

	public enum GameState {
		UNINITIALIZED, COIN_FLIP, START_HALF, BEFORE_TEAM_PLACEMENT, TEAM_PLACEMENT, KICKOFF, TOUCH_BACK, RECEIVING_TEAM, KICKING_TEAM, END_HALF, GAME_END
	}

	public static final int MAX_TURNS = 15;

	/** The private instance of the class */
	private static GameEngine m_instance;
	/** The playing field */
	private Pitch m_playingField;
	private boolean gameEnd = false;

	// we will always have two team on the playing field, so I
	private Team m_teamA = null;
	private Team m_teamB = null;

	private int m_halfCounter;
	private int m_turnCounter;
	private GameState m_gameState;
	private TeamID m_currentTeam;
	private TeamID m_otherTeam;
	private TeamID m_kickingTeam;

	private boolean exiting = false;

	/**
	 * AI engine instances.
	 */
	private final Map<TeamID, AIEngine> aiEngines = new HashMap<TeamID, AIEngine>(2, 1f);

	/**
	 * Id of engine's instance.
	 */
	private int engineInstanceId;

	transient private PitchGUI m_gui;

	public int turnCounter() {
		return m_turnCounter;
	}

	// FIXME: This should not be here!! It should probably be stored in the
	// player
	private ActionType m_action = ActionType.NONE;
	private Player m_lastSelectedPlayer = null;

	// the teams are exposed read only.
	// Any action performed on the team has to go through the engine
	@Override
	public final Team teamA() {
		return m_teamA;
	}

	@Override
	public final Team teamB() {
		return m_teamB;
	}

	public final Team getTeam(final TeamID teamID) {
		switch (teamID) {
		case TEAM_A:
			return m_teamA;
		case TEAM_B:
			return m_teamB;
		}
		assert false;
		return null;
	}

	/** Gets a reference to the single instance of the class */
	public static GameEngine getInstance() {
		return m_instance;
	}

	public final Team getOppositeTeam(final TeamID teamID) {
		switch (teamID) {
		case TEAM_B:
			return m_teamA;
		case TEAM_A:
			return m_teamB;
		}
		assert false;
		return null;
	}

	/**
	 * Creates a new instance.
	 * 
	 * @return instance
	 * 
	 * @author Tomasz Dąbrowski
	 */
	public static GameEngine createInstance() {
		if (m_instance != null) {
			m_instance.setGameEnd(true);
		}
		if (DataLoader.getInstance().isTeamAutoSelect()) {
			DataLoader.getInstance().loadDefaultTeams();
		}
		m_instance = new GameEngine();
		return m_instance;
	}

	/** Default constructor, shows GUI. */
	private GameEngine() {
		this(true);
	}

	/**
	 * Constructor that either shows or not the GUI.
	 * 
	 * @param showGUI
	 *            true if the GUI must be shown, false otherwise
	 */
	GameEngine(final boolean showGUI) {
		resetEngineInstanceId();

		// make sure the teams are destroyed
		m_teamA = null;
		m_teamB = null;

		// initialize the team counter and the game state
		m_halfCounter = 0;
		m_turnCounter = 0;
		m_gameState = GameState.UNINITIALIZED;
		m_currentTeam = TeamID.TEAM_A;
		m_otherTeam = TeamID.TEAM_B;
		m_kickingTeam = TeamID.TEAM_A;

		m_gui = PitchGUI.getInstance();
		m_gui.setStatus("Select teams and start game");
		m_gui.updateTopBar(PitchGUI.ButtonCardState.DISABLED);

	}

	@Override
	public Object clone() throws CloneNotSupportedException {
		throw new CloneNotSupportedException("The game engine class is a singleton and cannot be cloned.");
	}

	// /**
	// * On shutdown method.
	// *
	// * @author Tomasz Dąbrowski
	// */
	// public void onShutdown() {
	// synchronized (AIEngine.getThreadsLock()) {
	// m_instance = null;
	// PitchGUI.getInstance().dispose();
	// }
	// }

	/**
	 * Getter for the "exiting" property.
	 * 
	 * @return the exiting
	 * 
	 * @author Tomasz Dąbrowski
	 */
	@Override
	public boolean isExiting() {
		return exiting;
	}

	/**
	 * Getter for the "engineInstanceId" property.
	 * 
	 * @return the engineInstanceId
	 * 
	 * @author Tomasz Dąbrowski
	 */
	public int getEngineInstanceId() {
		return engineInstanceId;
	}

	/**
	 * Resets the "engineInstanceId" property.
	 * 
	 * @author Tomasz Dąbrowski
	 */
	public void resetEngineInstanceId() {
		engineInstanceId = (int) (Math.random() * Integer.MAX_VALUE);
		// AIEngine.wakeUpThreads();
	}

	/**
	 * Getter for the "m_playingField" property.
	 * 
	 * @return the m_playingField
	 */
	@Override
	public Pitch getPlayingField() {
		return m_playingField;
	}

	/**
	 * Sets the teams.
	 * 
	 * @param id
	 *            - the team ID
	 * @param team
	 *            - the team object
	 */
	public void setTeam(final TeamID id, final Team team) {
		switch (id) {
		case TEAM_A:
			m_teamA = team;
			break;
		case TEAM_B:
			m_teamB = team;
			break;
		}

		// update the gui to inform the player
		if (m_teamA != null && m_teamB != null && m_gameState == GameState.UNINITIALIZED) {
			m_gui.updateTopBar(PitchGUI.ButtonCardState.START_GAME);
		}
	}

	/** Initializes each team with a default composition. */
	public void createTeam(final String name, final Race race) {
		if (m_teamA == null) {
			m_teamA = new Team(name, race);
			m_teamA.resetToDefault();
		} else {
			m_teamB = new Team(name, race);
			m_teamB.resetToDefault();
		}
	}

	/**
	 * Starts a new game.
	 * 
	 * NOTE: This method should always be called after the teams have been
	 * chosen.
	 */
	public void startNewGame() throws Exception {
		if (m_teamA == null || m_teamB == null) {
			throw new Exception("The teams have not been initialized yet!");
		}

		LogWindow.instance().Log("Starting game");

		m_playingField = new Pitch(m_teamA, m_teamB);

		// register the gui as an observer for the map
		m_playingField.registerObserver(m_gui);

		m_turnCounter = 0;
		m_teamA.setScore(0);
		m_teamB.setScore(0);
		setGameState(GameState.COIN_FLIP);

		m_gui.setScore(m_teamA.getScore(), m_teamB.getScore());
		m_gui.setTurnCounter(m_turnCounter);
		m_gui.updateTopBar(PitchGUI.ButtonCardState.END_TURN);

		update();

	}

	void startTestGame(final Team teamA, final Team teamB) {
		if (teamA == null || teamB == null) {
			return;
		}

		m_teamA = teamA;
		m_teamB = teamB;

		m_playingField = new Pitch(m_teamA, m_teamB);

		m_kickingTeam = TeamID.TEAM_B;
		m_currentTeam = m_kickingTeam;
		m_otherTeam = TeamID.TEAM_A;

		setGameState(GameState.RECEIVING_TEAM);
	}

	/**
	 * Manages the game internal state. Please refer to the GameStateMachine.png
	 * image for a visual representation. Note : this method should always be
	 * called after input from the user.
	 */
	public void update() {
		while (!gameEnd) {
			final GameState initialState = getGameState();

			switch (initialState) {
			case COIN_FLIP: {
				for (Player p : teamA().players()) {
					p.setState(PlayerState.IN_DUGOUT);
				}
				for (Player p : teamB().players()) {
					p.setState(PlayerState.IN_DUGOUT);
				}
				m_turnCounter = 0;
				// Perform coin flip
				final int flip = Dice.rollDice(2);
				final TeamID choosingTeam = (flip == TeamID.TEAM_A.value() ? TeamID.TEAM_A : TeamID.TEAM_B);
				final TeamID passiveTeam = (flip == TeamID.TEAM_A.value() ? TeamID.TEAM_B : TeamID.TEAM_A);

				LogWindow.instance().Log("Coin flip in favor of team " + choosingTeam);

				final AIEngine choosingTeamAI = aiEngines.get(choosingTeam);
				if (choosingTeamAI != null) {
					m_kickingTeam = choosingTeamAI.isKickOffKickingTeam() ? choosingTeam : passiveTeam;
					m_currentTeam = m_kickingTeam;
					m_otherTeam = m_kickingTeam == choosingTeam ? passiveTeam : choosingTeam;
				} else {
					final String[] choices = new String[] { "Receive", "Kick" };
					final String selectedChoice = (String) JOptionPane.showInputDialog(m_gui, choosingTeam
							+ " do you want to receive or kick ? ", "Choose to receive or kick", JOptionPane.PLAIN_MESSAGE, null, choices,
							choices[0]);
					m_kickingTeam = (selectedChoice.compareTo(choices[0]) == 0 ? passiveTeam : choosingTeam);
					m_currentTeam = m_kickingTeam;
					m_otherTeam = (selectedChoice.compareTo(choices[0]) == 0 ? choosingTeam : passiveTeam);
				}
				LogWindow.instance().Log("Game starts, team " + m_otherTeam + " will receive the ball.");
				m_gui.setStatus(m_currentTeam.toString());
				setGameState(GameState.START_HALF);
			}
				break;

			case START_HALF: {
				// Start placement phase
				setGameState(GameState.BEFORE_TEAM_PLACEMENT);

				m_teamA.resetState(m_turnCounter);
				m_teamB.resetState(m_turnCounter);
			}
				break;

			case BEFORE_TEAM_PLACEMENT: {
				// For each team, before team placement:
				// -- Each player in the KO dugout has a 4-6 chance on a
				// D6 to
				// get
				// out of the KO dugout
				for (int i = 0; i < 2; i++) {
					final Team team = (i == 0 ? m_teamA : m_teamB);
					for (final Player player : team.players()) {
						if (player.state().equals(PlayerState.KO)) {
							final int roll = Dice.rollDice(6);
							LogWindow.instance().Log("> KO recovery roll: " + roll + " <");
							if (roll > 3) {
								LogWindow.instance().Log("Player " + player.name() + " gets back in the game!");
								player.setState(PlayerState.IN_DUGOUT);
							} else {
								LogWindow.instance().Log("Player " + player.name() + " is still out cold!");
							}
						}
					}
				}

				// clear the playing field
				m_playingField.clearField();

				// Reset position counters for the placement for both
				// teams
				m_teamA.setLeftWideZone(0);
				m_teamA.setRightWideZone(0);
				m_teamA.setScrimmageNum(0);

				m_teamB.setLeftWideZone(0);
				m_teamB.setRightWideZone(0);
				m_teamB.setScrimmageNum(0);

				for (AIEngine ai : aiEngines.values()) {
					ai.createGroups();
				}

				setGameState(GameState.TEAM_PLACEMENT);
			}
				break;

			case TEAM_PLACEMENT: {
				placementPhase();
			}
				break;

			case KICKOFF: {
				// Notify the kicking team that he must select the
				// landing
				// square of
				// the ball.
				// After selection, perform scatter, and start game!
				m_gui.setStatus(m_currentTeam.toString() + " select the ball landing square ");
				m_gui.setSelectionHighlight(new SelectionObject(currentTeamID() == TeamID.TEAM_A ? Zones.LEFT_PITCH : Zones.RIGHT_PITCH));

				final AIEngine aiEngine = aiEngines.get(currentTeamID());
				if (aiEngine != null) {
					processInput(aiEngine.getKickOffBallPosition());
				}
			}
				break;

			case TOUCH_BACK: {
				// In this case, the coach can select only one of his
				// players
				m_gui.setStatus(m_currentTeam.toString() + " select the player to get the ball.");

				final ArrayList<Position> playerPositions = new ArrayList<Position>();
				for (final Player player : currentTeam().players()) {
					if (player.state() == PlayerState.IN_PLAY) {
						playerPositions.add(player);
					}
				}

				m_gui.setSelectionHighlight(new SelectionObject(playerPositions, true));
			}
				break;

			case RECEIVING_TEAM:
			case KICKING_TEAM: {
				// First: check if all turns are finished in the half,
				// if so,
				// skip
				// directly there
				if (m_turnCounter > MAX_TURNS) {
					setGameState(GameState.END_HALF);
					break;
				}
			}
				break;

			case END_HALF: {

				m_turnCounter = 0;
				m_gui.setTurnCounter(m_turnCounter);

				m_halfCounter++;
				assert m_halfCounter <= 2;
				if (m_halfCounter == 2) {
					setGameState(GameState.GAME_END);
				} else {
					setGameState(GameState.START_HALF);

					// Player kicking in the second half shouldn't be
					// the same
					// as in
					// the first half.
					if (m_currentTeam == m_kickingTeam) {
						switchTeam();
					}
				}
			}
				break;

			case GAME_END: {
				aiEngines.clear();
				gameEnd = true;
				if (PitchGUI.isAutomaticMode()) {
					Thread endThread = new Thread() {
						@Override
						public void run() {
							for (AIEngine i : aiEngines.values()) {
								while (i.getGameThread().isAlive()) {
									try {
										i.getGameThread().join();
									} catch (InterruptedException e) {
									}
								}
							}
							try {
								sleep(1000);
							} catch (InterruptedException e) {
							}
							GameEngine.createInstance().startNewGame(DataLoader.getInstance().getTeamA(),
									DataLoader.getInstance().getTeamB());
						};
					};
					endThread.start();
					return;
				}
				if (m_teamA.getScore() == m_teamB.getScore()) {
					JOptionPane.showMessageDialog(m_gui, "The game ends in a draw", "Game end", JOptionPane.OK_OPTION);
				} else if (m_teamA.getScore() > m_teamB.getScore()) {
					JOptionPane.showMessageDialog(m_gui, "Team A wins the game!", "Game end", JOptionPane.OK_OPTION);
				} else {
					// teamA score < teamB score
					JOptionPane.showMessageDialog(m_gui, "Team B wins the game!", "Game end", JOptionPane.OK_OPTION);
				}

				// Assign MVP (i.e. 5 SPP)
				// TODO : have the condition
				// "was able to play during the match"
				// here
				final int teamAMVP_idx = Dice.rollDice(m_teamA.players().size()) - 1;
				final Player teamAMVP = m_teamA.players().get(teamAMVP_idx);
				teamAMVP.setXP(teamAMVP.XP() + 5);

				final int teamBMVP_idx = Dice.rollDice(m_teamB.players().size()) - 1;
				final Player teamBMVP = m_teamB.players().get(teamBMVP_idx);
				teamBMVP.setXP(teamBMVP.XP() + 5);

				JOptionPane.showMessageDialog(m_gui, "Players " + teamAMVP.name() + " from team A and " + teamBMVP.name()
						+ " from team B are awarded MVP!", "Game end", JOptionPane.OK_OPTION);

				// Update these players so that it shows in their
				// tooltip
				m_playingField.notifyObservers(teamAMVP);
				m_playingField.notifyObservers(teamBMVP);

				// Remove any dead players from the teams
				m_teamA.removeDeadPlayers();
				m_teamB.removeDeadPlayers();

				// Save the teams
				m_teamA.save();
				m_teamB.save();
			}
			}

			// Force reentrancy if the game state has changed
			if (initialState == getGameState()) {
				break;
			}

		}
	}

	/**
	 * Getter for the "gameEnd" property.
	 * 
	 * @return the gameEnd
	 * 
	 * @author Tomasz Dąbrowski
	 */
	@Override
	public boolean isGameEnd() {
		return gameEnd;
	}

	/**
	 * Setter for the "gameEnd" property.
	 * 
	 * @param gameEnd
	 *            the gameEnd to set
	 * 
	 * @author Tomasz Dąbrowski
	 */
	public void setGameEnd(final boolean gameEnd) {
		this.gameEnd = gameEnd;
		m_gui = null;
	}

	/**
	 * Separate implementation for the placement phase to prevent code
	 * clutter-up in the game state machine. Manages the states from one team
	 * placement to the other and the eventual selection highlights by team.
	 * 
	 */
	private void placementPhase() {
		// A few checks here:
		// 1 - If current team has finished placing its players, either switch
		// to next team or finish placement
		if (currentTeam().countPlayersByState(PlayerState.IN_PLAY) == Pitch.MAX_PLAYERS_ON_FIELD
				|| currentTeam().countPlayersByState(PlayerState.IN_DUGOUT) == 0) {
			m_gui.clearSelectionHighlight();

			if (otherTeam().countPlayersByState(PlayerState.IN_PLAY) != 0) {
				setGameState(GameState.KICKOFF);
			}

			// Switch back to KICKING team
			switchTeam();
		}
		// Initial half-pitch restriction visual: players must be on the
		// correct
		// side!
		if (currentTeam().countPlayersByState(PlayerState.IN_PLAY) == 0) {
			m_gui.setSelectionHighlight(new SelectionObject(currentTeamID() == TeamID.TEAM_A ? Zones.RIGHT_PITCH : Zones.LEFT_PITCH));
		}
	}

	/**
	 * Responds to input. Currently only supports input from the buttons!
	 * 
	 * @param currentPosition
	 *            Position of the button (x,y)
	 */
	public void processInput(final Position currentPosition) {
		switch (getGameState()) {
		case TEAM_PLACEMENT: {
			final Player player = m_gui.getSelectedReserve(currentTeamID());

			try {
				placePlayer(player, currentPosition);
				update();
			} catch (final IllegalPlacementException e) {
				LogWindow.instance().Log(e.getMessage());
			} catch (final BadPositionException e) {
				LogWindow.instance().Log(e.getMessage());
			}
		}
			break;

		case KICKOFF: {
			// First, the kickoff scatters 1D6 distance from its intended target
			Position kickPosition = ActionManager.scatterKick(currentPosition);

			boolean kickoffOk = true;
			boolean ballCaught = false;

			if (isInsideTeamZone(kickPosition, m_otherTeam)) {
				final Player catcher = m_playingField.getPlayer(kickPosition);
				if (catcher != null && catcher.state() == PlayerState.IN_PLAY) {
					final ActionManager.CatchBallResult catchResult = ActionManager.catchBall(catcher, false);
					ballCaught = catchResult.Success;
					m_playingField.notifyObservers(catcher);
				} else {
					// If the kick falls into an empty square, it scatters once
					kickPosition = ActionManager.scatterBall(kickPosition);

					// After scattering if it's still in the pitch, resolve the
					// bounce normally
					kickoffOk = isInsideTeamZone(kickPosition, m_otherTeam);
				}

				if (kickoffOk && !ballCaught) {
					resolveBounce(kickPosition, false, true);
				}
			} else {
				kickoffOk = false;
			}

			// After kickoff, the receiving team is now acting
			switchTeam();

			// If the kick was valid, then the receiving team starts playing
			if (kickoffOk) {
				// State is change to the normal game state
				setGameState(GameState.RECEIVING_TEAM);
				m_gui.clearSelectionHighlight();
			} else {
				// If kick was invalid, we pass into the touch back phase,
				// i.e. where the receiving player selects who in his team gets
				// the ball
				LogWindow.instance().Log("Kick is outside of bounds, proceeding to touch back!");
				setGameState(GameState.TOUCH_BACK);
				m_gui.clearSelectionHighlight();
			}
		}
			break;

		case TOUCH_BACK: {
			final Player player = m_playingField.getPlayer(currentPosition);
			if (player != null && currentTeam().players().contains(player)) {
				onTouchBack(player);
			} else {
				LogWindow.instance().Log("Please select a player from the receiving team");
			}
		}
			break;

		case RECEIVING_TEAM:
		case KICKING_TEAM: {
			// Route actions depending on the action and selection.
			// If no one selected: if target is a player, select it
			// If someone selected:
			// - If target == selected, unselect
			// - If not, apply action if any
			final Player player = currentTeam().getSelectedPlayer();
			final Player target = m_playingField.getPlayer(currentPosition);

			if (player == null || (player != null && player.equals(target))) {
				if (target != null && currentTeam().players().contains(target)
						&& (!target.hasActed() || target.hasActed() && target.hasBlitzed())) {
					// Un/Select the player
					target.select(!target.isSelected());

					// Update visuals
					m_playingField.notifyObservers(target);

					// Open action dialog
					if (target.isSelected()) {
						final ActionDialog actionDlg = new ActionDialog(m_gui, target);
						m_action = actionDlg.getAction();
					}

					if (!target.isSelected() || m_action.equals(ActionType.NONE)) {
						selectAction(target, ActionType.NONE);
					} else {
						// If we perform an action with another player, the last
						// player we moved or acted with
						// has now completed his turn.
						if (m_lastSelectedPlayer != null && !target.equals(m_lastSelectedPlayer)
								&& m_lastSelectedPlayer.currentMA() != m_lastSelectedPlayer.MA()) {
							m_lastSelectedPlayer.setHasActed(true);
							m_playingField.notifyObservers(m_lastSelectedPlayer);
						}

						m_lastSelectedPlayer = target;
					}
				}
			} else if (m_action.equals(ActionType.NONE)) {
				// Nothing ---
				LogWindow.instance().Log("Not supposed to come here");
			} else {
				boolean success = false;

				switch (m_action) {
				case MOVE:
					if (success = movePlayer(player, currentPosition)) {
						update();
						player.select(false);
						m_playingField.notifyObservers(currentPosition);

						if (isTouchDown(player)) {
							processTouchdown(player);
						}
					}
					break;
				case THROW:
					success = resolveThrowBall(player, currentPosition);
					break;
				case BLOCK:
					success = resolveBlock(player, currentPosition);
					break;
				case STAND:
					// Unreachable state.. see SelectionAction instead
					break;
				case BLITZ:
					// TODO Not completed
					success = resolveBlitz(player, currentPosition);
					break;
				default:
					LogWindow.instance().Log("Action " + m_action + " not implemented.");
				}

				// reset the current action
				if (success) {
					selectAction(null, ActionType.NONE);
				}
			}
		}
			break;
		}

		// Call an update of the state machine which is ABSOLUTELY NECESSARY. DO
		// NOT REMOVE.
		update();
	}

	/**
	 * On touch back.
	 * 
	 * @param player
	 *            selected player
	 * 
	 * @author Tomasz Dąbrowski
	 */
	@Override
	public void onTouchBack(final Player player) {
		player.setHasBall(true);
		m_playingField.notifyObservers(player);
		setGameState(GameState.RECEIVING_TEAM);
		m_gui.clearSelectionHighlight();
	}

	/**
	 * This method will be called when a button from the ActionDialog is pressed
	 * (or activated using the keyboard)
	 * 
	 * @param player
	 *            The player starting the action
	 * @param action
	 *            The selected action
	 */
	public void selectAction(final Player player, final ActionType action) {
		m_action = action;

		// Used in targetless actions
		boolean success = false;

		switch (action) {
		case NONE:
			if (player != null) {
				player.select(false);
				m_playingField.notifyObservers(player);
			}

			m_gui.clearActionHighlight();
			break;

		case MOVE:
			assert (player != null);
			m_gui.setActionHighlight(new MoveSelectionObject(player));
			break;

		case THROW:
			assert (player != null && player.hasBall());
			// Put highlight
			m_gui.setActionHighlight(new ThrowSelectionObject(player));
			break;

		case BLOCK:
			assert (player != null);
			// Put highlight
			m_gui.setActionHighlight(new BlockSelectionObject(player));
			break;

		case STAND:
			success = processStandup(player);
			break;

		case BLITZ:
			assert (player != null);
			// Put highlight
			m_gui.setActionHighlight(new BlockSelectionObject(player));
			break;
		}

		if (success) {
			m_action = ActionType.NONE;
		}
	}

	/**
	 * Presents the player with the possible outcomes of the block and resolves
	 * the action.
	 * 
	 * @param blocker
	 *            - the player performing the block
	 * @param target
	 *            - the target location on the pitch
	 * @return Returns true if the action was performed, false otherwise
	 */
	@Override
	public boolean resolveBlock(final Player blocker, final Position target) {
		boolean result = false;

		if (blocker.state() == PlayerState.STUNNED || blocker.state() == PlayerState.PRONE) {
			LogWindow.instance().Log("Player " + blocker.name() + " is Prone(Stunned) and cannot block");
		} else if (blocker.currentMA() > 0) {
			final Player victim = getPlayer(target);
			if (victim == null) {
				return result;
			}

			// can only block a player that is not knocked down
			if (victim.state() == PlayerState.STUNNED || victim.state() == PlayerState.PRONE) {
				LogWindow.instance().Log("The player " + victim.name() + " is already down and cannot be blocked.");
				return result;
			}

			// by default use one dice and the blocker chooses the outcome
			// (equal strength)
			int noDice = 1;
			TeamID choosingTeam = currentTeamID();

			// determine how many dices are being used and who chooses the
			// outcome
			if (blocker.ST() > 2 * victim.ST()) {
				noDice = 3;
			} else if (blocker.ST() > victim.ST()) {
				noDice = 2;
			} else if (2 * blocker.ST() < victim.ST()) {
				noDice = 3;
				choosingTeam = otherTeamID();
			} else if (blocker.ST() < victim.ST()) {
				noDice = 2;
				choosingTeam = otherTeamID();
			}

			// allow the player to choose the outcome

			BlockActionType action;
			List<BlockActionType> blockActions = new ArrayList<BlockActionType>(4);
			for (int i = 0; i < noDice; i++) {
				blockActions.add(Dice.rollBlockDice());
			}
			AIEngine ai = aiEngines.get(choosingTeam);
			if (ai == null) {
				final BlockActionDialog blockDlg = new BlockActionDialog(m_gui, choosingTeam, blockActions);
				action = blockDlg.getAction();
			} else {
				action = ai.getBlockAction(blockActions);
			}

			// the block costs one action point
			blocker.setCurrentMA(blocker.currentMA() - 1);
			// mark the player as hasActed and unselect it
			blocker.setHasActed(true);
			blocker.select(false);

			// resolve the action
			result = resolveBlockAction(action, blocker, victim, true);

			// make sure the blocker and the victim are updated in all cases
			m_playingField.notifyObservers(blocker);
			m_playingField.notifyObservers(victim);

			// if the blocker did not succeed, it's a turnover
			if (blocker.state() != PlayerState.IN_PLAY) {
				endTurn();
			}
		}

		return result;
	}

	/**
	 * Resolves the block action for the victim and the blocker.
	 * 
	 * @param action
	 *            - the action to resolve
	 * @param blocker
	 *            - the blocker
	 * @param victim
	 *            - the victim
	 * @param firstBlock
	 *            - specifies if the current blocker is the first in a chain of
	 *            blocks
	 * @return Returns true of the action is resolved
	 */
	private boolean resolveBlockAction(final BlockActionType action, final Player blocker, final Player victim, final boolean firstBlock) {
		boolean checkCasualties = false;

		switch (action) {
		case ATTACKER_DOWN:
			// the attacker is knock Out
			resolveKnockDown(blocker);
			checkCasualties = true;
			break;
		case BOTH_DOWN:
			if (blocker.hasSkill(Skill.BLOCK) && victim.hasSkill(Skill.BLOCK)) {
				LogWindow.instance().Log("Both players have the Block skill and are unharmed after the block.");
				return true;
			}

			if (!blocker.hasSkill(Skill.BLOCK)) {
				resolveKnockDown(blocker);
				checkCasualties = true;
			} else {
				LogWindow.instance().Log("Player " + blocker.name() + " used the Block skill and avoided being knocked out");
			}

			if (!victim.hasSkill(Skill.BLOCK)) {
				resolveKnockDown(victim);
				checkCasualties = true;
			} else {
				LogWindow.instance().Log("Player " + victim.name() + " used the Block skill and avoided being knocked out");
			}
			break;
		case PUSHED:
			resolvePushAction(blocker, victim, firstBlock);
			break;
		case DEFENDER_STUMBLES:
			resolvePushAction(blocker, victim, firstBlock);

			if (!victim.hasSkill(Skill.DODGE)) {
				// the victim couldn't dodge the block, so is knocked down
				// In the event that the victim has been pushed off the pitch,
				// he's already injured.
				// Don't check for knockdown twice!
				if (victim.state() != PlayerState.INJURED && victim.state() != PlayerState.DEAD) {
					checkCasualties = true;
					resolveKnockDown(victim);
				}
			} else {
				LogWindow.instance().Log("Player " + victim.name() + " used the Dodge skill and avoided being knocked out");
			}
			break;
		case DEFENDER_DOWN:
			resolvePushAction(blocker, victim, firstBlock);

			// If the victim has been pushed off the pitch, there's no need to
			// check
			// again for knockdown
			if (victim.state() != PlayerState.INJURED && victim.state() != PlayerState.DEAD) {
				checkCasualties = true;
				resolveKnockDown(victim);
			}
			break;
		}

		if (checkCasualties) {
			// If a player causes an opposing player to be removed as a Casualty
			// then they earn 2 Star Player points.
			// The points are only earned if the player blocks an opponent or is
			// blocked by an opponent himself.
			// Casualties inflicted in any other way (including injuries
			// inflicted by the crowd or from attacks with
			// chainsaws, bombs, or the Stab skill) do not count for Star Player
			// points.
			if (victim.state() == PlayerState.INJURED || victim.state() == PlayerState.DEAD) {
				LogWindow.instance().Log("Player " + blocker.name() + " is awarded 2 XP for the casualty!");
				blocker.setXP(blocker.XP() + 2);
			}

			if (blocker.state() == PlayerState.INJURED || blocker.state() == PlayerState.DEAD) {
				LogWindow.instance().Log("Player " + victim.name() + " is awarded 2 XP for the casualty!");
				victim.setXP(victim.XP() + 2);
			}
		}

		return true;
	}

	/**
	 * Resolves the push action
	 * 
	 * @param pusher
	 *            - the pusher
	 * @param pushed
	 *            - the player being pushed
	 * @param firstPush
	 *            - specifies if the current pusher is the first in a chain of
	 *            push actions
	 */
	private void resolvePushAction(final Player pusher, final Player pushed, final boolean firstPush) {
		try {
			AIEngine aiEngine = aiEngines.get(currentTeamID());
			Position destination;
			if (aiEngine != null) {
				destination = aiEngine.getPushDestination(pusher, pushed);
			} else {
				final PushActionDialog pushDlg = new PushActionDialog(m_gui, pushed, pusher, getValidPushDirection(pusher, pushed));
				destination = pushDlg.getPosition();
			}
			// if the destination is occupied, push that player away
			Player p;
			if ((p = getPlayer(destination)) != null) {
				resolveBlockAction(BlockActionType.PUSHED, pushed, p, false);
			} else if (m_playingField.isBall(destination)) {
				resolveBounce(destination);
			}

			final Position originalTarget = new Position(pushed.x(), pushed.y());

			if (Pitch.isInsidePitch(destination)) {
				// move the pushed player
				m_playingField.movePlayer(pushed, destination);
				LogWindow.instance().Log("Player " + pushed.name() + " was pushed to " + destination);

				// present the follow up option to the player
				if (firstPush && pusher.state() != PlayerState.PRONE && pusher.state() != PlayerState.STUNNED) {
					// display the JOptionPane showConfirmDialog
					int reply;
					if (aiEngine != null) {
						reply = aiEngine.followEnemy(pusher, pushed) ? JOptionPane.YES_NO_OPTION : JOptionPane.NO_OPTION;
					} else {
						reply = JOptionPane.showConfirmDialog(null, "Do you want to follow your victim?", "Follow Up?",
								JOptionPane.YES_NO_OPTION);
					}
					if (reply == JOptionPane.YES_OPTION) {
						m_playingField.movePlayer(pusher, originalTarget);
					} else {
						m_playingField.notifyObservers(pusher);
					}
				}
			} else // push outside the pitch
			{
				LogWindow.instance()
						.Log("Player " + pushed.name() + " was pushed to outside the pitch. The crowd gave him a good beating!");

				// The victim is beaten by the crowds and goes to the injured
				// dugout
				resolveKnockDown(pushed, false);

				// throw the ball back on the pitch
				if (pushed.hasBall()) {
					resolveBounce(originalTarget, true, false);
					pushed.setHasBall(false);
				}
			}
		} catch (final IllegalPlacementException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (final BadPositionException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (final Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/**
	 * Gets the list of available directions for the push.
	 * 
	 * @param pusher
	 *            - the pusher position
	 * @param pushed
	 *            - the pushed position
	 * @return Returns a list of directions
	 * @throws Exception
	 *             - throws an exception if the resulting direction is invalid
	 */
	private ArrayList<Direction> getValidPushDirection(final Player pusher, final Player pushed) throws Exception {
		final ArrayList<Direction> ret = new ArrayList<Direction>();

		final Position opposite = m_playingField.getOppositePosition(pushed, pusher);

		final int xDiff = opposite.x() - pushed.x();
		final int yDiff = opposite.y() - pushed.y();

		boolean foundFavourableDirection = false;
		Position pos;

		// west
		if (xDiff == -1) {
			// north
			if (yDiff == -1) {
				pos = convertDirectionToPosition(pushed, Direction.NW);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.NW);
					foundFavourableDirection = true;
				}
				pos = convertDirectionToPosition(pushed, Direction.W);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.W);
					foundFavourableDirection = true;
				}
				pos = convertDirectionToPosition(pushed, Direction.N);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.N);
					foundFavourableDirection = true;
				}
				// if no empty location was found, add all occupied locations
				if (!foundFavourableDirection) {
					ret.add(Direction.NW);
					ret.add(Direction.N);
					ret.add(Direction.W);
				}
			}
			// center
			else if (yDiff == 0) {
				pos = convertDirectionToPosition(pushed, Direction.W);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.W);
					foundFavourableDirection = true;
				}
				pos = convertDirectionToPosition(pushed, Direction.NW);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.NW);
					foundFavourableDirection = true;
				}
				pos = convertDirectionToPosition(pushed, Direction.SW);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.SW);
					foundFavourableDirection = true;
				}
				// if no empty location was found, add all occupied locations
				if (!foundFavourableDirection) {
					ret.add(Direction.W);
					ret.add(Direction.NW);
					ret.add(Direction.SW);
				}
			}
			// south
			else if (yDiff == 1) {
				pos = convertDirectionToPosition(pushed, Direction.SW);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.SW);
					foundFavourableDirection = true;
				}
				pos = convertDirectionToPosition(pushed, Direction.W);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.W);
					foundFavourableDirection = true;
				}
				pos = convertDirectionToPosition(pushed, Direction.S);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.S);
					foundFavourableDirection = true;
				}
				// if no empty location was found, add all occupied locations
				if (!foundFavourableDirection) {
					ret.add(Direction.SW);
					ret.add(Direction.W);
					ret.add(Direction.S);
				}
			}
		}
		// center
		else if (xDiff == 0) {
			// north
			if (yDiff == -1) {
				pos = convertDirectionToPosition(pushed, Direction.N);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.N);
					foundFavourableDirection = true;
				}
				pos = convertDirectionToPosition(pushed, Direction.NW);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.NW);
					foundFavourableDirection = true;
				}
				pos = convertDirectionToPosition(pushed, Direction.NE);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.NE);
					foundFavourableDirection = true;
				}
				// if no empty location was found, add all occupied locations
				if (!foundFavourableDirection) {
					ret.add(Direction.N);
					ret.add(Direction.NW);
					ret.add(Direction.NE);
				}
			}
			// center
			else if (yDiff == 0) {
				throw new Exception("Invalid push direction");
			}
			// south
			else if (yDiff == 1) {
				pos = convertDirectionToPosition(pushed, Direction.S);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.S);
					foundFavourableDirection = true;
				}
				pos = convertDirectionToPosition(pushed, Direction.SW);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.SW);
					foundFavourableDirection = true;
				}
				pos = convertDirectionToPosition(pushed, Direction.SE);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.SE);
					foundFavourableDirection = true;
				}
				// if no empty location was found, add all occupied locations
				if (!foundFavourableDirection) {
					ret.add(Direction.S);
					ret.add(Direction.SW);
					ret.add(Direction.SE);
				}
			}
		}
		// east
		else if (xDiff == 1) {
			// north
			if (yDiff == -1) {
				pos = convertDirectionToPosition(pushed, Direction.NE);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.NE);
					foundFavourableDirection = true;
				}
				pos = convertDirectionToPosition(pushed, Direction.N);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.N);
					foundFavourableDirection = true;
				}
				pos = convertDirectionToPosition(pushed, Direction.E);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.E);
					foundFavourableDirection = true;
				}
				// if no empty location was found, add all occupied locations
				if (!foundFavourableDirection) {
					ret.add(Direction.NE);
					ret.add(Direction.N);
					ret.add(Direction.E);
				}
			}
			// center
			else if (yDiff == 0) {
				pos = convertDirectionToPosition(pushed, Direction.E);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.E);
					foundFavourableDirection = true;
				}
				pos = convertDirectionToPosition(pushed, Direction.NE);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.NE);
					foundFavourableDirection = true;
				}
				pos = convertDirectionToPosition(pushed, Direction.SE);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.SE);
					foundFavourableDirection = true;
				}
				// if no empty location was found, add all occupied locations
				if (!foundFavourableDirection) {
					ret.add(Direction.E);
					ret.add(Direction.NE);
					ret.add(Direction.SE);
				}
			}
			// south
			else if (yDiff == 1) {
				pos = convertDirectionToPosition(pushed, Direction.SE);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.SE);
					foundFavourableDirection = true;
				}
				pos = convertDirectionToPosition(pushed, Direction.S);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.S);
					foundFavourableDirection = true;
				}
				pos = convertDirectionToPosition(pushed, Direction.E);
				if (!Pitch.isInsidePitch(pos) || m_playingField.isEmpty(pos) || m_playingField.isBall(pos)) {
					ret.add(Direction.E);
					foundFavourableDirection = true;
				}
				// if no empty location was found, add all occupied locations
				if (!foundFavourableDirection) {
					ret.add(Direction.SE);
					ret.add(Direction.S);
					ret.add(Direction.E);
				}
			}
		}

		return ret;
	}

	/**
	 * Processes the "Blitz" action
	 * 
	 * @param blitzer
	 *            The player who blitz the opponent player
	 * @param target
	 *            The destination of the move/blitz
	 * @return true if the operation was successful, false if not
	 */
	private boolean resolveBlitz(final Player blitzer, final Position target) {
		boolean result = false;
		if (currentTeam().hasBlitzed()) {
			LogWindow.instance().Log("Team " + currentTeamID() + " used its Blitz Action!!No more Blitz in this turn!");
			return result;
		}
		if (blitzer.state() == PlayerState.PRONE || blitzer.state() == PlayerState.STUNNED) {
			LogWindow.instance().Log("Player " + blitzer.name() + " is prone or stunned and can not Blitz.");
			return result;
		} else if (blitzer.currentMA() > 0) {
			final Player victim = getPlayer(target);
			if (victim == null) {
				return result;
			}

			result = resolveBlock(blitzer, target);

			// only set the hasBlitzed property if the blitzer is unharmed
			// otherwise, the turn has ended and the teams have switched
			if (result && blitzer.state() == PlayerState.IN_PLAY) {
				currentTeam().setHasBlitzed();
				blitzer.setHasBlitzed(true);
			}
		}
		// The current team has now successfully Blitz the opponent, and can't
		// Blitz again in this turn.
		return result;
	}

	/**
	 * Processes the "Throw" action
	 * 
	 * @param thrower
	 *            The player throwing the ball
	 * @param target
	 *            The destination of the ball
	 * @return true if the operation was successful, false if not
	 */
	@Override
	public boolean resolveThrowBall(final Player thrower, final Position target) {
		assert (!currentTeam().hasThrown());

		if (thrower.state() == PlayerState.PRONE || thrower.state() == PlayerState.STUNNED) {
			LogWindow.instance().Log("Player " + thrower.name() + " is prone or stunned.");
			return false;
		}

		// Throw ball
		final ActionManager.ThrowBallResult throwResult = ActionManager.throwBall(thrower, target);

		// If the target position is too far, the result will be null
		if (throwResult == null) {
			return false;
		}

		// Resolve action
		boolean ballCaught = false;
		Player catcher = null;
		final Position ballPosition = throwResult.BallPosition;
		final boolean isOutsidePitch = throwResult.IsOutsidePitch;

		if (!isOutsidePitch) {
			catcher = getPlayer(ballPosition);

			if (catcher != null && catcher.state() == PlayerState.IN_PLAY) {
				final ActionManager.CatchBallResult catchResult = ActionManager.catchBall(catcher, throwResult.IsAccurate);
				ballCaught = catchResult.Success;
			}
		}

		// If ball wasn't caught, resolve as a bounce
		if (!ballCaught) {
			resolveBounce(ballPosition, isOutsidePitch, false);
		} else if (ballCaught) {
			m_playingField.notifyObservers(catcher);
		}

		// Finally:
		// If the pass was accurate and the intended catcher got it,
		// Then the thrower is awarded 1 SPP for the completion
		if (ballCaught && throwResult.IsAccurate) {
			LogWindow.instance().Log("Player " + thrower.name() + " is awarded 1 SPP for the completion.");
			thrower.setXP(thrower.XP() + 1);
		}

		// Force visual update
		m_playingField.notifyObservers(thrower);

		// The current team has now successfully thrown the ball, and can't
		// throw it again in this turn
		currentTeam().setHasThrown();

		// If the moving team doesn't have the ball at the end of the throw,
		// then there is a turnover
		if (!currentTeam().hasBall()) {
			endTurn();
		}

		return true;
	}

	/**
	 * Resolves the ball bouncing in the general case
	 * 
	 * @param ballOrigin
	 *            - the location of the ball before the scatter
	 */
	private void resolveBounce(final Position ballOrigin) {
		assert (Pitch.isInsidePitch(ballOrigin));
		resolveBounce(ballOrigin, false, false);
	}

	/**
	 * Resolves the ball bouncing and throw-ins
	 * 
	 * @param ballOrigin
	 *            The ball position before scatter
	 * @param isOutsidePitch
	 *            The ball is outside the pitch and will be thrown back in
	 * @param isAlreadyScattered
	 *            The ball was already scattered
	 */
	private void resolveBounce(final Position ballOrigin, final boolean isOutsidePitch, final boolean isAlreadyScattered) {
		assert (Pitch.isInsidePitch(ballOrigin));
		boolean forceThrowin = isOutsidePitch;

		Position ballPrevious = new Position(ballOrigin);
		Position ballPosition;

		// make sure the original position is cleared
		if (m_playingField.isBall(ballOrigin)) {
			m_playingField.clearCell(ballOrigin);
		}

		if (isOutsidePitch || isAlreadyScattered) {
			ballPosition = new Position(ballOrigin);
		} else {
			ballPosition = ActionManager.scatterBall(ballPrevious);
			LogWindow.instance().Log("Ball scatters from " + ballPrevious.toString() + " to " + ballPosition.toString() + ".");
		}

		boolean ballStopped = false;

		while (!ballStopped) {
			// Ball scattered in the pitch
			if (Pitch.isInsidePitch(ballPosition) && !forceThrowin) {
				// If there is a player, it tries to catch the ball
				final Player catcher = getPlayer(ballPosition);
				if (catcher != null) {
					if (catcher.state() == PlayerState.IN_PLAY) {
						final ActionManager.CatchBallResult catchResult = ActionManager.catchBall(catcher, false);
						ballStopped = catchResult.Success;
					}

					if (!ballStopped) {
						ballPrevious = ballPosition;
						ballPosition = ActionManager.scatterBall(ballPrevious);
						LogWindow.instance().Log("Ball scatters from " + ballPrevious.toString() + " to " + ballPosition.toString() + ".");
					}
				} else // If not, then the ball stops there
				{
					assert (m_playingField.getPlayer(ballPosition) == null);
					try {
						m_playingField.placeBall(ballPosition);
						ballStopped = true;
					} catch (final BadPositionException e) {
						LogWindow.instance().Log(e.getMessage());
					}
				}
			} else // ball outside the pitch, perform throw-in
			{
				forceThrowin = false;
				ballPrevious = ActionManager.findLastValidPosition(ballPrevious, ballPosition);
				ballPosition = ActionManager.throwin(ballPrevious);
				LogWindow.instance().Log("Ball is thrown-in from " + ballPrevious.toString() + " to " + ballPosition.toString() + ".");

				// If the throwin is in an empty square, we must scatter it once
				if (Pitch.isInsidePitch(ballPosition) && getPlayer(ballPosition) == null) {
					ballPrevious = ballPosition;
					ballPosition = ActionManager.scatterBall(ballPrevious);
					LogWindow.instance().Log("Ball scatters from " + ballPrevious.toString() + " to " + ballPosition.toString() + ".");
				}
			}
		}

		m_playingField.notifyObservers(ballOrigin);
		m_playingField.notifyObservers(ballPosition);
	}

	/**
	 * Performs all necessary step following a touchdown by a player (assigning
	 * XP, changing state, etc.)
	 * 
	 * @param player
	 *            The player that did the touchdown
	 */
	private void processTouchdown(final Player player) {
		// Precondition
		assert (isTouchDown(player));

		// TODO Do whatever needs to be done for a touchdown
		currentTeam().setScore(currentTeam().getScore() + 1);
		m_gui.setScore(m_teamA.getScore(), m_teamB.getScore());
		// Remove ball from player
		player.setHasBall(false);

		if (!PitchGUI.isAutomaticMode()) {
			JOptionPane.showMessageDialog(m_gui, player.name() + " scores a touchdown!", "Touchdown", JOptionPane.OK_OPTION);
		}

		// Player that performed the touchdown is awarded 3 SPP
		player.setXP(player.XP() + 3);
		LogWindow.instance().Log("Player " + player.name() + " is awarded 3 SPP for the touchdown.");

		// Change state on a touchdown
		endTurn();
		if (getGameState() == GameState.KICKING_TEAM || getGameState() == GameState.RECEIVING_TEAM) {
			switchTeam();
			setGameState(GameState.BEFORE_TEAM_PLACEMENT);
		}
		update();
	}

	/**
	 * Performs the "standup" operation on the player
	 * 
	 * @param player
	 *            The player trying to standup
	 * @return true if the operation was successfully completed
	 */
	@Override
	public boolean processStandup(final Player player) {
		if (player.state() != PlayerState.PRONE) {
			LogWindow.instance().Log("A player that isn't prone can't stand up!");
			return false;
		}

		if (player.currentMA() >= 3) {
			player.setCurrentMA(player.currentMA() - 3);
			player.setState(PlayerState.IN_PLAY);
			LogWindow.instance().Log("Player " + player.name() + " stands up using 3 MA points");
		} else {
			// A player can stand up if he succeeds a 4+ roll
			final int standup_roll = Dice.rollDice(6);
			LogWindow.instance().Log("> Standup roll: " + standup_roll + " vs 4 < ");

			if (standup_roll >= 4) {
				player.setState(PlayerState.IN_PLAY);
				LogWindow.instance().Log("Player " + player.name() + " stands up succesfully!");
			} else {
				// To prevent re-entrancy
				player.setHasActed(true);
				LogWindow.instance().Log("Player " + player.name() + " fails to stand up.");
			}
		}

		// Unselect player
		player.select(false);

		// Update player visual
		m_playingField.notifyObservers(player);

		return true;
	}

	/**
	 * Check and process if touchdown.
	 * 
	 * @param player
	 *            player
	 * @return is touchdown
	 * 
	 * @author Tomasz Dąbrowski
	 */
	/*
	 * (non-Javadoc)
	 * 
	 * @see engine.IGameEngine#checkForTouchDown(data.Player)
	 */
	@Override
	public boolean checkForTouchDown(final Player player) {
		boolean touchdown = isTouchDown(player);
		if (touchdown) {
			processTouchdown(player);
		}
		return touchdown;
	}

	/**
	 * Checks if the player is in a touchdown position
	 * 
	 * @param player
	 *            - the player to check
	 * @return Returns true if the player did a touchdown, false otherwise
	 */
	public boolean isTouchDown(final Player player) {
		if (player.hasBall()
				&& ((m_teamA.players().contains(player) && Pitch.isEndZoneB(player)) || (m_teamB.players().contains(player) && Pitch
						.isEndZoneA(player)))) {
			return true;
		}

		return false;
	}

	/**
	 * Starts a new game with the selected teams.
	 * 
	 * @param teamA
	 *            team A
	 * @param teamB
	 *            team B
	 */
	public void startNewGame(final Team teamA, final Team teamB) {
		synchronized (AIEngine.getThreadsLock()) {
			m_teamA = teamA;
			if (GameMenuBar.getInstance().isTeamAAiControlled()) {
				aiEngines.put(teamA.getId(), AIEngine.createInstance(teamA, this));
			}
			m_teamB = teamB;
			if (GameMenuBar.getInstance().isTeamBAiControlled()) {
				aiEngines.put(teamB.getId(), AIEngine.createInstance(teamB, this));
			}
			try {
				startNewGame();
				m_gui.updateTopBar(PitchGUI.ButtonCardState.END_TURN);
			} catch (final Exception e) {
				// No foreseen exception
			}
		}
	}

	/**
	 * Getter for the "aiEngines" property.
	 * 
	 * @return the aiEngines
	 * 
	 * @author Tomasz Dąbrowski
	 */
	public AIEngine getAiEngine(final TeamID teamID) {
		return aiEngines.get(teamID);
	}

	/**
	 * Changes the game state.
	 * 
	 * @param state
	 *            - the new state.
	 */
	private void setGameState(final GameState state) {
		m_gameState = state;
	}

	/**
	 * Returns the current game state
	 * 
	 * @return GameState - the current game state
	 */
	@Override
	public GameState getGameState() {
		return m_gameState;
	}

	/**
	 * Changes the current team.
	 */
	private void switchTeam() {
		final TeamID tempTeam = m_currentTeam;
		m_currentTeam = m_otherTeam;
		m_otherTeam = tempTeam;

		m_gui.setStatus(m_currentTeam.toString());
		AIEngine aiEngine = aiEngines.get(currentTeamID());
		if (aiEngine != null) {
			aiEngine.onSwitchTeam();
		}
	}

	public boolean isInitialized() {
		return !(m_gameState == GameState.UNINITIALIZED);
	}

	public boolean isPlacementState() {
		return m_gameState == GameState.TEAM_PLACEMENT;
	}

	public boolean isGameRunning() {
		return m_gameState == GameState.RECEIVING_TEAM || m_gameState == GameState.KICKING_TEAM;
	}

	/**
	 * Gets the current team;
	 * 
	 * @return Returns a reference to the current team.
	 */
	@Override
	public Team currentTeam() {
		if (m_currentTeam == TeamID.TEAM_A) {
			return m_teamA;
		} else {
			return m_teamB;
		}
	}

	/**
	 * Gets the other team;
	 * 
	 * @return Returns a reference to the other team.
	 */
	public Team otherTeam() {
		return (m_otherTeam == TeamID.TEAM_A ? m_teamA : m_teamB);
	}

	/**
	 * Gets the team to which the player belongs.
	 * 
	 * @param player
	 *            - the given player
	 * @return Returns the team to which the player belongs
	 */
	public Team getPlayerTeam(final Player player) {
		if (m_teamA.players().contains(player)) {
			return m_teamA;
		} else {
			return m_teamB;
		}
	}

	/**
	 * Gets the current team ID.
	 * 
	 * @return Returns the team ID of the team currently active.
	 */
	@Override
	public final TeamID currentTeamID() {
		return m_currentTeam;
	}

	/**
	 * Gets the other team ID.
	 * 
	 * @return Returns the team ID of the team not active.
	 */
	public final TeamID otherTeamID() {
		return m_otherTeam;
	}

	/**
	 * Gets the players team ID.
	 * 
	 * @param player
	 *            - the given player
	 * @return Returns the player's team ID.
	 */
	public final TeamID getPlayerTeamID(final Player player) {
		if (getPlayerTeam(player).equals(currentTeam())) {
			return currentTeamID();
		} else {
			return otherTeamID();
		}
	}

	/**
	 * Performs all the actions required a the end of turn.
	 */
	@Override
	public void endTurn() {
		final GameState gameState = getGameState();

		if (gameState != GameState.RECEIVING_TEAM && gameState != GameState.KICKING_TEAM) {
			return;
		}

		currentTeam().resetState(m_turnCounter);
		// Force visual update
		for (final Player player : currentTeam().players()) {
			m_playingField.notifyObservers(player);
		}

		switchTeam();
		m_turnCounter++;
		m_lastSelectedPlayer = null;
		m_gui.setTurnCounter(m_turnCounter);
		setGameState(gameState == GameState.RECEIVING_TEAM ? GameState.KICKING_TEAM : GameState.RECEIVING_TEAM);
		m_action = ActionType.NONE;
		update();
		LogWindow.instance().Log("## A new turn has started! Current team: " + m_currentTeam + "##");
	}

	/**
	 * Gets the player located at the specified position.
	 * 
	 * @param pos
	 *            - the position of the player
	 * @return Returns a player. If there is no player at the desired location,
	 *         returns null
	 */
	public Player getPlayer(final Position pos) {
		return m_playingField.getPlayer(pos);
	}

	/**
	 * Places a player on the field.
	 * 
	 * @param player
	 *            - the player to place
	 * @param pos
	 *            - the position of the player on the field.
	 * @throws IllegalPlacementException
	 * @throws BadPositionException
	 */
	@Override
	public void placePlayer(final Player player, final Position pos) throws IllegalPlacementException, BadPositionException {
		// When placing players, perform first the verification (i.e. throwing
		// exceptions)
		// Then perform placement (which can throw an error if there's already
		// someone in the square
		// And finally, update the counts!

		// First check: the player to be placed is on the good half of the
		// pitch.
		if (m_currentTeam.equals(TeamID.TEAM_A) && Pitch.isZoneTeamB(pos) || m_currentTeam.equals(TeamID.TEAM_B) && Pitch.isZoneTeamA(pos)) {
			throw new BadPositionException("All players must be placed on their team's side of the pitch");
		}

		// Second check: if placing the player would make it impossible to have
		// 3 players on the scrimmage line, then we must abort!
		final int nbRemainingPlayersToPlace = Math.min(currentTeam().countPlayersByState(PlayerState.IN_DUGOUT), Pitch.MAX_PLAYERS_ON_FIELD
				- currentTeam().countPlayersByState(PlayerState.IN_PLAY));
		if (nbRemainingPlayersToPlace + currentTeam().getScrimmageNum() <= 3 && !Pitch.isScrimmageZone(pos)) {
			throw new BadPositionException(
					"Remaining players MUST be placed on the scrimmage line for a valid team placement! (Min. 3 on scrimmage line)");
		} else if (Pitch.isTopWideZone(pos) && currentTeam().getLeftWideZone() >= 2) {
			throw new BadPositionException("Up wide zone has its maximum number of players (2).");
		} else if (Pitch.isBottomWideZone(pos) && currentTeam().getRightWideZone() >= 2) {
			throw new BadPositionException("Bottom wide zone has its maximum number of players (2)");
		}

		// Perform actual placement of the player on the pitch
		// Note that this will throw an exception if there is already a player
		// in that square.
		m_playingField.placePlayer(m_currentTeam, player, pos);

		// Update counts (needed for placement checking, visuals, ... )
		if (Pitch.isScrimmageZone(pos)) {
			currentTeam().setScrimmageNum(currentTeam().getScrimmageNum() + 1);
		} else if (Pitch.isTopWideZone(pos)) {
			currentTeam().setLeftWideZone(currentTeam().getLeftWideZone() + 1);
			// When there is no more possible player placement in that zone, we
			// gray it out.
			if (currentTeam().getLeftWideZone() >= 2) {
				m_gui.setSelectionHighlight(new SelectionObject(Zones.TOP_WIDE));
			}
		} else if (Pitch.isBottomWideZone(pos)) {
			currentTeam().setRightWideZone(currentTeam().getRightWideZone() + 1);
			// When there is no more possible player placement in that zone, we
			// gray it out.
			if (currentTeam().getRightWideZone() >= 2) {
				m_gui.setSelectionHighlight(new SelectionObject(Zones.BOTTOM_WIDE));
			}
		}

		// Put the visual restriction for the scrimmage if the remaining players
		// to be placed MUST be on the scrimmage line.
		if (nbRemainingPlayersToPlace - 1 + currentTeam().getScrimmageNum() <= 3 && !Pitch.isScrimmageZone(pos)) {
			m_gui.setSelectionHighlight(new SelectionObject(Zones.SCRIMMAGE_LINE, true));
		}

		// Remove player from the reserve
		m_gui.removeSelectedReserve(currentTeamID());

		LogWindow.instance().Log("Player " + player.name() + " placed at " + pos.toString());
		update();
	}

	/**
	 * Moves a player on the field.
	 * 
	 * @param player
	 *            - the player to be moved
	 * @param newPos
	 *            - the new position of the player
	 * @throws TeamException
	 * @throws IllegalPlacementException
	 * @throws BadPositionException
	 * @return True if the move was carried out or false if not.
	 */
	@Override
	public boolean movePlayer(final Player player, final Position newPos) {
		// Does player belong to the current team?
		if (!currentTeam().players().contains(player)) {
			LogWindow.instance().Log("Player does not belong to active team");
			return false;
		}

		if (player.state() == PlayerState.PRONE || player.state() == PlayerState.STUNNED) {
			LogWindow.instance().Log("Player " + player.name() + " is prone or stunned.");
			return false;
		}

		if (player.currentMA() < 1) {
			LogWindow.instance().Log("Player " + player.name() + " has no remaining MA and therefore can't move.");
			return false;
		}

		if (!canMoveTo(player, newPos)) {
			LogWindow.instance().Log("Player cannot move to target square; either it is already occupied or it is too far away");
			return false;
		}

		// Perform move action
		LogWindow.instance().Log("Player " + player.name() + " moves to " + newPos);
		final boolean isBall = m_playingField.isBall(newPos);
		final boolean mustDodgeOut = (GameEngine.getInstance().countEnemyTackleZones(player) > 0);

		try {
			m_playingField.movePlayer(player, newPos);
			player.setCurrentMA(player.currentMA() - 1);
		} catch (final BadPositionException e) {
			// No foreseen exception
			LogWindow.instance().Log(e.getMessage());
			return false;
		} catch (final IllegalPlacementException e) {
			// No foreseen exception
			LogWindow.instance().Log(e.getMessage());
			return false;
		}

		if (mustDodgeOut) {
			LogWindow.instance().Log(player.name() + " has " + countEnemyTackleZones(player) + " opposing players ready to tackle!");
		}

		boolean turnover = false;

		// If we move onto the ball, check if we're able to pick it up correctly
		if (isBall) {
			final boolean pickedUp = ActionManager.pickupBall(player);
			if (!pickedUp) {
				// Remove ball from player
				player.setHasBall(false);
				// Perform bouncing
				resolveBounce(player);

				// Visual update needed if the player wasn't able to pick up the
				// ball
				m_playingField.notifyObservers(player);
				turnover = true;
			}
		}

		// There is a dodge roll only if the square the player is leaving is
		// "threatened" by a player from the other team
		// The modifier for the roll depends on the number of players
		// threatening the new square that the player moves to.
		if (mustDodgeOut && ActionManager.isTackled(player)) {
			// On a knock down, there is ALWAYS a turnover
			turnover = true;

			resolveKnockDown(player);
		}

		if (turnover) {
			endTurn();
		}

		// update();

		return true;
	}

	/**
	 * Resolves the player knock down when the player can always make an armor
	 * roll.
	 * 
	 * @param player
	 *            - the player to resolve
	 */
	private void resolveKnockDown(final Player player) {
		resolveKnockDown(player, true);
	}

	/**
	 * Resolves the player knock down situation
	 * 
	 * @param player
	 *            - the player to resolve
	 * @param makesArmorRoll
	 *            - specifies if the player is allowed to make an armour roll
	 */
	private void resolveKnockDown(final Player player, final boolean makesArmorRoll) {
		// A knock down == being placed prone
		player.setState(PlayerState.PRONE);
		m_playingField.notifyObservers(player);

		// Remove ball from player if he had it, bounce the ball one square.
		if (player.hasBall()) {
			player.setHasBall(false);
			// Perform bouncing
			resolveBounce(player);
		}

		// Player has been knocked down, so a roll of two D6s must be made
		// against the player armor rating
		int armor_roll = -1;
		if (makesArmorRoll) {
			armor_roll = Dice.rollDice(6, 2);
			LogWindow.instance().Log("> Armor roll: " + armor_roll + " vs " + player.AV() + " <");
		}

		// If the roll beats the player AV, then we roll for injury
		if (armor_roll > player.AV() || !makesArmorRoll) {
			// Player has been knocked down, so a roll of two D6s must be made
			// to see if he is injured
			// 2-7: Stunned | 8-9: KnockedOut | 10-12: Casualty
			final int injury_roll = Dice.rollDice(6, 2);
			LogWindow.instance().Log("> Injury roll: " + injury_roll + " <");

			if (injury_roll <= 7) {
				player.setState(PlayerState.STUNNED);
				player.setWasStunnedOnTurn(m_turnCounter);

				// if the player is allowed to make an armor roll, he is allowed
				// to stay on the field
				// if not, he is sent to the dugout and has to wait until the
				// break to come back in the game
				if (makesArmorRoll) {
					m_playingField.notifyObservers(player);
					LogWindow.instance().Log("Player " + player.name() + " is stunned! ");
				} else {
					m_playingField.removePlayerFromField(getPlayerTeamID(player), player);
					m_playingField.notifyObservers(player);
					m_playingField.resetObservers();
					LogWindow.instance().Log("Player " + player.name() + " is stunned and returns to the dugout!");
				}
			} else if (injury_roll <= 9) {
				// Knock Out
				player.setState(PlayerState.KO);
				m_playingField.removePlayerFromField(getPlayerTeamID(player), player);
				m_playingField.notifyObservers(player);
				m_playingField.resetObservers();
				LogWindow.instance().Log("Player " + player.name() + " is knocked out and goes to the KO dugout! ");
			} else {
				// Dead & injured : technically, either INJURED or DEAD
				final int casualty_roll = Dice.rollDice(6) * 10 + Dice.rollDice(8);
				LogWindow.instance().Log("> Casualty roll: " + casualty_roll + " <");

				if (casualty_roll >= 61) {
					LogWindow.instance().Log("Player " + player.name() + " dies of his injuries!!!");
					player.setState(PlayerState.DEAD);
				} else {
					LogWindow.instance().Log("Player " + player.name() + " is seriously injured!");
					player.setState(PlayerState.INJURED);
				}
				m_playingField.removePlayerFromField(getPlayerTeamID(player), player);
				m_playingField.notifyObservers(player);
				m_playingField.resetObservers();

				LogWindow.instance().Log("Player " + player.name() + " is dead or injured and goes to the Dead & Injured dugout! ");
			}
		} else {
			LogWindow.instance().Log("Player " + player.name() + " is lucky! No injuries were sustained in the fall.");
		}
	}

	/**
	 * Checks if a player can move to the target position.
	 * 
	 * @param player
	 *            The current player (used to get the player position)
	 * @param newPos
	 *            The target position
	 * @return true if the player can move legally to that position, false
	 *         otherwise.
	 * 
	 *         This method checks the square by square restriction, if the
	 *         target destination is in the pitch and if the target square is
	 *         empty of players.
	 */
	public boolean canMoveTo(final Player player, final Position newPos) {
		return (Math.abs(newPos.x() - player.x()) <= 1 && Math.abs(newPos.y() - player.y()) <= 1 && Pitch.isInsidePitch(newPos) && m_playingField
				.getPlayer(newPos) == null);
	}

	/**
	 * Counts the number of enemies that can tackle the player.
	 * 
	 * @param player
	 *            - the player of interest
	 * @return Returns the number of enemies that can tackle the player.
	 */
	public int countEnemyTackleZones(final Player player) {
		return countEnemyTackleZones(player, player);
	}

	/**
	 * Counts the number of "enemies" threatening the given position
	 * 
	 * @param player
	 *            A member of the team threatened
	 * @param pos
	 *            The target position
	 * @return the number of players threatening this square
	 */
	public int countEnemyTackleZones(final Player player, final Position pos) {
		int tackleCount = 0;

		for (int x = pos.x() - 1; x <= pos.x() + 1; x++) {
			for (int y = pos.y() - 1; y <= pos.y() + 1; y++) {
				final Position adj = new Position(x, y);
				final Player tackler = getPlayer(adj);
				// Three conditions: 1 - there is a player
				// 2 - in a valid state (i.e. not prone or stunned)
				// 3 - from the other team
				if (tackler != null && tackler.state().equals(PlayerState.IN_PLAY)
						&& currentTeam().players().contains(player) != currentTeam().players().contains(tackler)) {
					tackleCount++;
				}
			}
		}

		return tackleCount;
	}

	/**
	 * Checks if the square is occupied by a player
	 * 
	 * @param pos
	 *            The target position
	 * @return true if the square is occupied, else otherwise.
	 */
	public boolean playerAt(final Position pos) {
		return (m_playingField.getPlayer(pos) != null);
	}

	public boolean isInsideTeamZone(final Position pos, final TeamID teamID) {
		if (teamID.equals(TeamID.TEAM_A)) {
			return Pitch.isZoneTeamA(pos);
		} else {
			return Pitch.isZoneTeamB(pos);
		}
	}

	/**
	 * Debug method. Serializes the current state of the map as a string for
	 * display on the command line
	 * 
	 * @return Returns a string.
	 */
	public String showTextMap() {
		return m_playingField.toString();
	}

	/**
	 * Saves the game to the given path.
	 * 
	 * @param path
	 *            - the path of the save game
	 * @return true if the save succeeds, false otherwise
	 */
	public boolean saveGame(final String path) {
		try {
			final ObjectOutputStream stream = new ObjectOutputStream(new FileOutputStream(path));
			stream.writeObject(m_instance);
			stream.close();

			LogWindow.instance().Log("Game saved");
		} catch (final FileNotFoundException e) {
			LogWindow.instance().Log("Save failed! See the log for details.");
			e.printStackTrace();
			return false;
		} catch (final IOException e) {
			LogWindow.instance().Log("Save failed! See the log for details.");
			e.printStackTrace();
			return false;
		}

		return true;
	}

	/**
	 * Loads a game from disk.
	 * 
	 * @param path
	 *            - the path to the save game.
	 * @return true if the load succeeds, false otherwise.
	 */
	public boolean loadGame(final String path) {
		try {
			// close the old before it will die
			m_instance.m_gui.dispose();

			// read the saved game
			final ObjectInputStream stream = new ObjectInputStream(new FileInputStream(path));
			m_instance = (GameEngine) stream.readObject();
			stream.close();

			// reset the gui
			// m_instance.m_gui = new PitchGUI(GUI_CAPTION, true);
			m_instance.m_gui = PitchGUI.getInstance();
			// FIXME: We should check that we are in the right state
			m_instance.m_gui.updateTopBar(ButtonCardState.END_TURN);
			m_instance.m_playingField.registerObserver(m_instance.m_gui);
			m_instance.m_playingField.resetObservers();

			LogWindow.instance().Log("Game loaded");
		} catch (final FileNotFoundException e) {
			LogWindow.instance().Log("Load failed! See the log for details.");
			e.printStackTrace();
			return false;
		} catch (final IOException e) {
			LogWindow.instance().Log("Load failed! See the log for details.");
			e.printStackTrace();
			return false;
		} catch (final ClassNotFoundException e) {
			LogWindow.instance().Log("Load failed! See the log for details.");
			e.printStackTrace();
			return false;
		}

		return true;
	}

	/**
	 * Triggers a map update.
	 */
	public void updateMap() {
		m_playingField.notifyObservers();
	}

	/**
	 * Converts the direction form a position into a position on the field.
	 * 
	 * @param center
	 *            - the fixed point
	 * @param dir
	 *            - the direction offset relative to the fixed point
	 * @return Returns a position offset by one location from the center in the
	 *         given direction.
	 */
	public Position convertDirectionToPosition(final Position center, final Direction dir) {
		return m_playingField.convertDirectionToPosition(center, dir);
	}
}
